Wasserbau 4, Uebung 02#

Aufgabe 1 – Interaktiver Wehrbedienungsplan#

Dieses interaktive Notebook erlaubt es, Parameter für eine Wehranlage selbst zu variieren und die Auswirkungen auf hydraulische Kenngrößen zu untersuchen.

Für eine geplante Wehranlage soll Ihr Ingenieurbüro die erforderlichen Berechnungen durchführen und den Wehrbedienungsplan erstellen. Folgende Werte wurden bestimmt: Das Wehr wird mit 5 Fischbauchklappen mit einer Breite von je 7,2 m und einem Verlustbeiwert von 𝜇 = 0,65 ausgebildet. Die Stauhöhe beträgt 10 m. Die Wehrhöhe ist auf 6 m festgelegt. Die Sohleintiefung beträgt 𝑠 = 2 m. Die Abflusskurve ist in der Excel-Tabelle (Anlage gegeben). Setzen Sie Verluste mit \(ℎ_𝑣\) = 0,2 ℎ′ an.

Bernoulli-Energiegleichung#

Die Bernoulli-Gleichung beschreibt die Energieerhaltung in einer idealen, reibungsfreien Strömung. Sie lautet:

\[ \frac{p_1}{\rho g} + \frac{v_1^2}{2g} + z_1 = \frac{p_2}{\rho g} + \frac{v_2^2}{2g} + z_2 + h_v \]

Bedeutung der Terme:#

  • \( \frac{p}{\rho g} \) ist der Druckhöhenanteil.

  • \( \frac{v^2}{2g} \) ist der Geschwindigkeits- oder dynamische Höhenanteil.

  • \( \ z_2 \) ist der geodätische Höhenanteil.

  • \( \ h_v \) ist die Verlusthöhe

Diese Gleichung beschreibt die Energieerhaltung entlang eines Stromfadens und setzt voraus, dass keine äußeren Kräfte oder Reibungsverluste wirken.

Der ebene freie Wechselsprung#

Durch die Anwendung des Impulssatzes an den Kontrollschnitten 1-1 und 2-2 kann die Gleichung der konjugierten Wassertiefen beim Wechselsprung hergeleitet werden

\[ \frac{h_2}{h_1} = \frac{1}{2} \left( \sqrt{1 + 8 \cdot Fr_1^2} - 1 \right) \quad bzw. \quad \frac{h_1}{h_2} = \frac{1}{2} \left( \sqrt{1 + 8 \cdot Fr_2^2} - 1 \right) \]

Durch die zusätzliche Anwendung der Bernoulli-Gleichung kann die Verlusthöhe im Wechselsprung wie folgt berechnet werden:

\[ h_i = \frac{(h_2 - h_1)^3}{4 \cdot h_1 \cdot h_2} \]

Aufgabe 1#

Aufgabe 1#

→ Es wurden bereits für drei in der Tabelle angegebene Abflüsse die erforderlichen Unterwassertiefen bei Ableitung über jeweils 4 Klappen bestimmt:

      a) Berechnen Sie die noch fehlenden Werte und zeichnen Sie den Wehrbedienungsplan

      b) Ist die Tiefe des Tosbeckens ausreichend bemessen?

      c) Über wie viele Klappen muss ein Abfluss von 𝑄 = 30 m³/s abgeführt werden?

Gegeben:#

n = 5        𝑏 = 7,2 m        𝜇 = 0,65        \(h_o\) = 10 m        𝑤 = 6 m        𝑠 = 2 m        \(ℎ_𝑣\) = 0,2 ⋅ ℎ′

Berechnung von \(h_{2,\text{erf}}\):#

\[ h' + \frac{v_0^2}{2 \cdot g} = h_1 + \frac{v_1^2}{2 \cdot g} + h_v \]
\[ h' = h_o + s \]
\[ Fr = \frac{v}{\sqrt{g \cdot h_1}} \]
\[ h_{2,\text{erf}} = \frac{h_1}{2} \left( \sqrt{1 + 8 \cdot Fr_r^2} - 1 \right) \]

→ Mindestwassertiefe des Unterwassers, damit das Bauwerk keinen Schaden nimmt

import ipywidgets as widgets
from ipywidgets import interact
from IPython.display import display

# Interaktive Eingabe der Parameter
mu_widget = widgets.FloatSlider(value=0.65, min=0.4, max=1.0, step=0.01, description='μ:')
n_widget = widgets.IntSlider(value=4, min=1, max=5, step=1, description='Klappen n:')
Q_widget = widgets.FloatSlider(value=440, min=10, max=500, step=10, description='Q [m³/s]:')
h1_widget = widgets.FloatSlider(value=1.19, min=1.0, max=2.0, step=0.005, description='h1 [m]:')

# Konstanten
b_klappe = 7.2  # Breite pro Klappe [m]
g = 9.81        # Erdbeschleunigung [m/s²]

#display(mu_widget, n_widget, Q_widget, h1_widget)

def berechne_interaktiv(mu, n, Q, h1):
    A = b_klappe * n
    v1 = Q / (A * h1)
    Fr = v1 / (g * h1)**0.5
    h2_erf = (h1 / 2) * ((8 * Fr**2 + 1)**0.5 - 1)
    print(f"Froude-Zahl Fr = {Fr:.2f}")
    print(f"Erforderliche Tiefe h2_erf = {h2_erf:.2f} m")
    
interact(berechne_interaktiv, mu=mu_widget, n=n_widget, Q=Q_widget, h1=h1_widget)
<function __main__.berechne_interaktiv(mu, n, Q, h1)>

✍ Aufgabe:#

Variieren Sie den Abfluss Q und beobachten Sie, wie sich die erforderliche Wassertiefe h2_erf verändert.

Was passiert bei hoher Froude-Zahl?

Wie verändert sich der Wehrbedienungsplan bei geringer Klappenzahl?

import pandas as pd
import plotly.express as px
import ipywidgets as widgets
from IPython.display import display

# Gegebene Daten für h2_erf und Abfluss bei verschiedenen Klappenanzahlen
data = {
    "h2_erf [m]": [1.49, 2.67, 4.09, 5.76],
    "n=1": [6, 20, 50, 110],
    "n=2": [12, 40, 100, 220],
    "n=3": [18, 60, 150, 330],
    "n=4": [24, 80, 200, 440],
    "n=5": [30, 100, 250, 550],
}

# Erstellen eines Pandas DataFrames aus dem Dictionary
df = pd.DataFrame(data)

# Funktion zum Anzeigen des Diagramms
def zeige_diagramm():
    # Erstellen einer Liste aller Spalten für die Klappenanzahlen
    spalten = [f"n={n}" for n in range(1, 6)]

    # Erstellen des Diagramms mit Plotly Express
    fig = px.line(df, y="h2_erf [m]", x=spalten,
                  title="Wehrbedienungsplan (Alle Klappen)",
                  labels={"h2_erf [m]": "h2_erf [m]", "value": "Abfluss Q [m³/s]", "variable": "Klappenanzahl"},
                  markers=True,
                  range_x=[0, 550],
                  range_y=[0, 6])

    # Anpassen des Layouts des Diagramms
    fig.update_layout(
        yaxis_title="h2_erf [m]",
        xaxis_title="Abfluss Q [m³/s]",
        yaxis_range=[0, 6],
        xaxis_range=[0, 550],
        yaxis_gridcolor='lightgrey',
        xaxis_gridcolor='lightgrey',
        plot_bgcolor='white'
    )

    # Anzeigen des Diagramms
    fig.show()

# Anzeigen des Diagramms (ohne interaktives Widget)
zeige_diagramm()

Plotly-Diagramme sind ein wertvolles Werkzeug, um hydraulische Daten interaktiv zu erkunden. Anders als statische Grafiken ermöglichen sie es, durch einfaches Hovern mit der Maus präzise Werte an einzelnen Datenpunkten abzulesen. Für Detailanalysen könnt ihr in bestimmte Bereiche zoomen oder das Diagramm verschieben. Ein wesentliches Feature ist die interaktive Legende: Einzelne Datensätze lassen sich durch einen Klick gezielt ein- oder ausblenden. Der Doppelklick auf einen Eintrag in der Legende isoliert diesen Datensatz, was den Vergleich mit anderen Datensätzen vereinfacht.

Im nächsten Schritt muss \(h_u\) bestimmt werden. Dies kann aus der Abflusskurve abgelesen werden. Markiere die entsprechenden Punkte für \(h_u\) im nächsten Diagramm!

import pandas as pd
import plotly.express as px
import dash
from dash import dcc, html, Input, Output

# Daten definieren
abflusskurve_data = {
    "Q": [0, 20, 24, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440],
    "hu": [0, 0.6, 0.66, 0.9, 1.15, 1.37, 1.59, 1.78, 1.97, 2.13, 2.3, 2.45, 2.6, 2.75, 2.89, 3.02, 3.15, 3.28, 3.4, 3.52, 3.64, 3.76, 3.82, 3.9]
}

df_abflusskurve = pd.DataFrame(abflusskurve_data)
richtige_punkte = [(24, 0.66), (80, 1.37), (200, 2.45), (440, 3.9)]

toleranz_Q = 10
toleranz_hu = 0.1

# Dash-App erstellen
app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Graph(id='abfluss-plot', config={'displayModeBar': False}),
    html.Div(id='rueckmeldung', style={'font-weight': 'bold', 'font-size': '18px', 'margin-top': '10px'}),
    html.Div(id='korrekte-punkte', style={'margin-top': '10px'}),
    html.Div(id='erfolg-nachricht', style={'font-weight': 'bold', 'font-size': '20px', 'color': 'green', 'margin-top': '20px'})
])

korrekt_ausgewaehlt = []

@app.callback(
    [Output('abfluss-plot', 'figure'), 
     Output('rueckmeldung', 'children'),
     Output('korrekte-punkte', 'children'),
     Output('erfolg-nachricht', 'children')],
    Input('abfluss-plot', 'clickData')
)
def update_plot(clickData):
    global korrekt_ausgewaehlt
    
    fig = px.line(df_abflusskurve, x='Q', y='hu', title="Finde die richtigen Punkte!",
                  labels={'Q': 'Abfluss Q [m³/s]', 'hu': 'hu [m]'}, line_shape='linear')
    fig.update_traces(line=dict(color='blue'))
    
    rueckmeldung = "Klicke auf einen Punkt in der Grafik."
    erfolg_nachricht = ""
    
    if clickData:
        clicked_x = clickData['points'][0]['x']
        clicked_y = clickData['points'][0]['y']
        
        richtig = any(abs(clicked_x - Q) <= toleranz_Q and abs(clicked_y - hu) <= toleranz_hu for Q, hu in richtige_punkte)
        
        if richtig:
            naechster_punkt = next((p for p in richtige_punkte if abs(clicked_x - p[0]) <= toleranz_Q and abs(clicked_y - p[1]) <= toleranz_hu), None)
            if naechster_punkt and naechster_punkt not in korrekt_ausgewaehlt:
                farbe = 'green'
                rueckmeldung = "✅ Punkt richtig!"
                korrekt_ausgewaehlt.append(naechster_punkt)
                if len(korrekt_ausgewaehlt) == len(richtige_punkte):
                    erfolg_nachricht = "Herzlichen Glückwunsch - alle Punkte gefunden!"
            else:
                farbe = 'orange'
                rueckmeldung = "Dieser Punkt wurde bereits ausgewählt."
        else:
            farbe = 'red'
            rueckmeldung = "❌ Punkt falsch!"
        
        fig.add_scatter(x=[clicked_x], y=[clicked_y], mode='markers', marker=dict(size=12, color=farbe))
    
    # Füge alle korrekt ausgewählten Punkte hinzu
    for x, y in korrekt_ausgewaehlt:
        fig.add_scatter(x=[x], y=[y], mode='markers', marker=dict(size=12, color='green'))
    
    korrekte_punkte_text = [
        html.Div([
            html.Span("h_u,", style={'font-size': '18px'}),
            html.Span(f"{int(x)}", style={'font-size': '14px', 'vertical-align': 'sub'}),
            html.Span(f" = {y:.2f}", style={'font-size': '18px'})
        ]) for x, y in korrekt_ausgewaehlt
    ]
    
    return fig, rueckmeldung, korrekte_punkte_text if korrekte_punkte_text else "", erfolg_nachricht

if __name__ == '__main__':
    app.run(debug=False)
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
Cell In[3], line 85
     82     return fig, rueckmeldung, korrekte_punkte_text if korrekte_punkte_text else "", erfolg_nachricht
     84 if __name__ == '__main__':
---> 85     app.run(debug=False)

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\dash\dash.py:2257, in Dash.run(self, host, port, proxy, debug, jupyter_mode, jupyter_width, jupyter_height, jupyter_server_url, dev_tools_ui, dev_tools_props_check, dev_tools_serve_dev_bundles, dev_tools_hot_reload, dev_tools_hot_reload_interval, dev_tools_hot_reload_watch_interval, dev_tools_hot_reload_max_retry, dev_tools_silence_routes_logging, dev_tools_disable_version_check, dev_tools_prune_errors, **flask_run_options)
   2254             extra_files.append(path)
   2256 if jupyter_dash.active:
-> 2257     jupyter_dash.run_app(
   2258         self,
   2259         mode=jupyter_mode,
   2260         width=jupyter_width,
   2261         height=jupyter_height,
   2262         host=host,
   2263         port=port,
   2264         server_url=jupyter_server_url,
   2265     )
   2266 else:
   2267     self.server.run(host=host, port=port, debug=debug, **flask_run_options)

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\dash\_jupyter.py:405, in JupyterDash.run_app(self, app, mode, width, height, host, port, server_url)
    403     display(HTML(msg))
    404 else:
--> 405     raise final_error

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\dash\_jupyter.py:392, in JupyterDash.run_app(self, app, mode, width, height, host, port, server_url)
    389         raise err
    391 try:
--> 392     wait_for_app()
    394     if self.in_colab:
    395         JupyterDash._display_in_colab(dashboard_url, port, mode, width, height)

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\retrying.py:56, in retry.<locals>.wrap.<locals>.wrapped_f(*args, **kw)
     54 @six.wraps(f)
     55 def wrapped_f(*args, **kw):
---> 56     return Retrying(*dargs, **dkw).call(f, *args, **kw)

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\retrying.py:266, in Retrying.call(self, fn, *args, **kwargs)
    263 if self.stop(attempt_number, delay_since_first_attempt_ms):
    264     if not self._wrap_exception and attempt.has_exception:
    265         # get() on an attempt with an exception should cause it to be raised, but raise just in case
--> 266         raise attempt.get()
    267     else:
    268         raise RetryError(attempt)

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\retrying.py:301, in Attempt.get(self, wrap_exception)
    299         raise RetryError(self)
    300     else:
--> 301         six.reraise(self.value[0], self.value[1], self.value[2])
    302 else:
    303     return self.value

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\six.py:724, in reraise(tp, value, tb)
    722     if value.__traceback__ is not tb:
    723         raise value.with_traceback(tb)
--> 724     raise value
    725 finally:
    726     value = None

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\retrying.py:251, in Retrying.call(self, fn, *args, **kwargs)
    248     self._before_attempts(attempt_number)
    250 try:
--> 251     attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
    252 except:
    253     tb = sys.exc_info()

File ~\anaconda3\envs\hausarbeit_wb4\lib\site-packages\dash\_jupyter.py:383, in JupyterDash.run_app.<locals>.wait_for_app()
    381     if res != "Alive":
    382         url = f"http://{host}:{port}"
--> 383         raise OSError(
    384             f"Address '{url}' already in use.\n"
    385             "    Try passing a different port to run."
    386         )
    387 except requests.ConnectionError as err:
    388     _get_error()

OSError: Address 'http://127.0.0.1:8050' already in use.
    Try passing a different port to run.
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import dash
from dash import dcc, html

# Daten für Wehrbedienungsplan
wehr_data = {
    "h2_erf [m]": [1.49, 2.67, 4.09, 5.76],
    "n=1": [6, 20, 50, 110],
    "n=2": [12, 40, 100, 220],
    "n=3": [18, 60, 150, 330],
    "n=4": [24, 80, 200, 440],
    "n=5": [30, 100, 250, 550],
}
df_wehr = pd.DataFrame(wehr_data)

# Daten für Abflusskurve
abflusskurve_data = {
    "Q": [0, 20, 24, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440],
    "hu": [0, 0.6, 0.66, 0.9, 1.15, 1.37, 1.59, 1.78, 1.97, 2.13, 2.3, 2.45, 2.6, 2.75, 2.89, 3.02, 3.15, 3.28, 3.4, 3.52, 3.64, 3.76, 3.82, 3.9]
}
df_abflusskurve = pd.DataFrame(abflusskurve_data)

# Abflusskurve + 2m berechnen
df_abflusskurve["hu+2"] = df_abflusskurve["hu"] + 2

# Funktion zum Finden der Schnittpunkte
def find_intersections(df_wehr, df_abflusskurve, tolerance=0.01):
    intersections = []
    for n in ["n=1", "n=2", "n=3"]:
        for i in range(len(df_wehr) - 1):
            x1, y1 = df_wehr[n][i], df_wehr["h2_erf [m]"][i]
            x2, y2 = df_wehr[n][i+1], df_wehr["h2_erf [m]"][i+1]
            
            for j in range(len(df_abflusskurve) - 1):
                x3, y3 = df_abflusskurve["Q"][j], df_abflusskurve["hu+2"][j]
                x4, y4 = df_abflusskurve["Q"][j+1], df_abflusskurve["hu+2"][j+1]
                
                denom = (y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)
                if abs(denom) > 1e-8:  # Vermeidung von Division durch Null
                    ua = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / denom
                    ub = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / denom
                    if 0 <= ua <= 1 and 0 <= ub <= 1:
                        x = x1 + ua * (x2-x1)
                        y = y1 + ua * (y2-y1)
                        if abs(y - (y3 + ub * (y4-y3))) < tolerance:
                            intersections.append({"n": n, "Q": x, "h": y})
    
    return intersections


intersections = find_intersections(df_wehr, df_abflusskurve)

# Dash-App erstellen
app = dash.Dash(__name__)

# Graph erstellen
fig = go.Figure()
farben = ['red', 'green', 'blue', 'purple', 'orange']
for i, col in enumerate(["n=1", "n=2", "n=3", "n=4", "n=5"]):
    fig.add_trace(go.Scatter(x=df_wehr[col], y=df_wehr["h2_erf [m]"], mode='lines+markers', name=col, line=dict(color=farben[i])))
fig.add_trace(go.Scatter(x=df_abflusskurve['Q'], y=df_abflusskurve['hu'], mode='lines', name='Abflusskurve', line=dict(color='darkgrey', width=2, dash='dash')))
fig.add_trace(go.Scatter(x=df_abflusskurve['Q'], y=df_abflusskurve['hu+2'], mode='lines', name='Abflusskurve +2m', line=dict(color='black', width=2, dash='dash')))

# Schnittpunkte zur Figur hinzufügen
for point in intersections:
    fig.add_trace(go.Scatter(
        x=[point['Q']],
        y=[point['h']],
        mode='markers',
        marker=dict(size=15, symbol='circle-open', color='red'),  
        name=f"Schnittpunkt {point['n']}",
        text=f"n={point['n']}, Q={point['Q']:.2f}, h={point['h']:.2f}",
        hoverinfo='text'
    ))

# Layout-Anpassungen
fig.update_layout(
    title="Wehrbedienungsplan und Abflusskurve mit Schnittpunkten",
    xaxis_title="Abfluss Q [m³/s]",
    yaxis_title="h2_erf [m] / hu + s [m]",
    plot_bgcolor='white',
    yaxis_range=[0, 6],
    xaxis_range=[0, 550],
    xaxis=dict(
        tickmode='linear',
        tick0=0,
        dtick=20,
        gridcolor='darkgrey',
        gridwidth=1.0,
        ticklen=5,
        minor=dict(
            tickmode='linear',
            tick0=0,
            dtick=5,
            gridcolor='lightgrey',
            gridwidth=0.5
        )
    ),
    yaxis=dict(
        tickmode='linear',
        tick0=0,
        dtick=1,
        gridcolor='darkgrey',
        gridwidth=1.0,
        minor=dict(
            dtick=0.2,
            gridcolor='lightgrey',
            gridwidth=0.2
        )
    ),
    legend_title="Legende",
    hovermode="closest"
)

app.layout = html.Div([
    html.H1("Wehrbedienungsplan und Abflusskurve"),
    dcc.Graph(id="graph", figure=fig),
    html.Div(id="output-container", style={"margin": "20px", "padding": "15px", "border": "1px solid gray"})
])

if __name__ == '__main__':
    app.run(debug=False)

Überfallhöhen#

Im nächsten Schritt wird die Überfallhöhe berechnet. Es gibt zwei verschiedene Überfallarten.

Vollkommener Überfall#

  • Der Abfluss Q wird nicht vom Unterwasser her beeinflusst

  • Im Bereich der Überfallkrone kommt es zum Fließwechsel Strömen → Schießen

  • Im Bereich der Wehrkrone herrscht der Grenzzustand (\(h_gr\))

Unvollkommener Überfall#

  • Der Abfluss Q kann vom Unterwasserstand beeinträchtigt bzw. vermindert werden (Abminderungsfaktor c)

  • Es kommt nicht zu einem Fließwechsel

\[ Q = \frac{2}{3} (\cdot \ c) \cdot \mu \cdot b \cdot \sqrt{2 \cdot g} \cdot h_ü^{\frac{3}{2}} \]
\[ b = n \cdot b \]
\[ \mu = 0.65, \, b = 7.2, \, g = 9.81 \]
import dash
from dash import dcc, html, Input, Output, State, dash_table
import pandas as pd
import math
import plotly.express as px

# Dash-App erstellen
app = dash.Dash(__name__)

# Formel zur Berechnung von Q
def berechne_Q(hu, n):
    µ = 0.65
    b = n * 7.2
    g = 9.81
    return (2 / 3) * µ * b * math.sqrt(2 * g) * (hu ** (3 / 2))

# Daten definieren
hü_values = [0, 1, 2, 3, 4]
n_values = [1, 2, 3, 4]

# Kontrollwerte aus der Tabelle
expected_values = {
    (hu, n): berechne_Q(hu, n) for hu in hü_values for n in n_values
}

# Leere Tabelle erstellen
df_table = pd.DataFrame(
    [[None for _ in n_values] for _ in hü_values],
    columns=[f"n_{n}" for n in n_values],
    index=hü_values
)
df_table.insert(0, "hü", hü_values)

# Speicherung der bisherigen Werte
previous_plot_data = []

app.layout = html.Div([
    
    dash_table.DataTable(
        id='interaktive-tabelle',
        columns=[{"name": "hü", "id": "hü", "editable": False}] + 
                [{"name": f"n = {n}", "id": f"n_{n}", "editable": True} for n in n_values],
        data=df_table.to_dict('records'),
        editable=True,
        style_table={'margin': '20px'},
        style_cell={'textAlign': 'center', 'font-size': '16px'},
        style_header={
            'backgroundColor': 'rgb(220, 220, 220)',
            'fontWeight': 'bold'
        },
        style_data_conditional=[
            {
                'if': {'row_index': 'odd'},
                'backgroundColor': 'rgb(248, 248, 248)'
            }
        ]
    ),
    
    html.Div(id='feedback', style={'margin-top': '20px', 'font-weight': 'bold', 'font-size': '18px'}),
    dcc.Graph(id='plot')
])

@app.callback(
    [Output('interaktive-tabelle', 'data'), Output('feedback', 'children'), Output('plot', 'figure')],
    Input('interaktive-tabelle', 'data'),
    State('interaktive-tabelle', 'data_previous')
)
def validate_table(data, data_previous):
    global previous_plot_data
    if data_previous is None:
        return data, "", px.line()
    
    feedback_messages = []
    new_plot_data = []
    
    for row in data:
        hu_value = row['hü']
        for n in n_values:
            col_name = f"n_{n}"
            cell_value = row[col_name]
            
            if cell_value is not None:
                try:
                    cell_value_float = float(cell_value)
                    expected_value = expected_values.get((hu_value, n), None)
                    if expected_value is not None and math.isclose(cell_value_float, expected_value, rel_tol=1e-2):
                        row[col_name] = f"{cell_value_float} ✅"
                        new_plot_data.append((cell_value_float, hu_value, f"n={n}"))
                    else:
                        row[col_name] = f"{cell_value_float} ❌"
                except ValueError:
                    continue
    
    # Kombiniere alte und neue Werte
    previous_plot_data.extend(new_plot_data)
    df_plot = pd.DataFrame(previous_plot_data, columns=['Q', 'hü', 'n'])
    fig = px.line(df_plot, x='Q', y='hü', color='n', title="Visualisierung der berechneten Q-Werte", markers=True)
    fig.update_layout(yaxis=dict(range=[0, max(hü_values)]), xaxis=dict(range=[0, max(expected_values.values())]))
    
    return data, "\n".join(feedback_messages), fig

if __name__ == '__main__':
    app.run(debug=False)